/**
 * 
 */
package it.unitn.testbed.server.task;

import it.unitn.testbed.server.TestbedServiceImpl;
import it.unitn.testbed.server.serial.MessageConsumer;
import it.unitn.testbed.server.serial.SerialReceiver;
import it.unitn.testbed.server.serial.TestbedClassLoader;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Properties;
import java.util.TimerTask;
import java.util.concurrent.Semaphore;

import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;

import org.apache.log4j.Logger;

/**
 * Denotes a task to be scheduled for execution.
 * 
 * When the task is executed, it first uses a custom classloader (
 * {@link TestbedClassLoader}) to load the classes denoting serial messages.
 * Then, it instantiates a new {@link SerialReceiver} object that handles any
 * incoming packets. After that, the task reprograms the motes as specified by a
 * list of {@link TaskComponent structures.}.
 * 
 * The task must be built by {@link TaskScheduler}.
 * 
 * @author Stefan Guna
 * @see TaskScheduler
 * @see TestbedClassLoader
 */
public class Task extends TimerTask implements UncaughtExceptionHandler {

	public static Logger log = Logger.getLogger(Task.class.getName());
	/** Mappings of binary image - motes. */
	private ArrayList<TaskComponent> components;
	/** Duration of the task. */
	private int duration;
	private Properties emailProperties;
	/** The semaphore to wait for the programming of all motes to complete */
	private Semaphore flashSem;

	List<MoteProgramException> programErrors;

	/** The serial receiver. */
	private SerialReceiver receiver;
	/** Files containing classes denoting serial messages. */
	private HashMap<String, File> serialMsgs;

	private TaskScheduler taskScheduler;

	private TestbedServiceImpl testbedService;

	/**
	 * Instantiates a new task object.
	 * 
	 * @param components
	 *            Mappings of binary image - motes.
	 * @param serialMsgs
	 *            Files containing classes denoting serial messages.
	 * @param duration
	 *            Duration of the task.
	 * @param testbedServiceImpl
	 *            Callback object.
	 * @param taskScheduler
	 *            Callback object.
	 */
	Task(ArrayList<TaskComponent> components, HashMap<String, File> serialMsgs,
			int duration, TestbedServiceImpl testbedServiceImpl,
			TaskScheduler taskScheduler) {
		this.components = components;
		this.serialMsgs = serialMsgs;
		log.trace("New task");
		this.duration = duration;
		this.testbedService = testbedServiceImpl;
		this.taskScheduler = taskScheduler;
		programErrors = Collections
				.synchronizedList(new ArrayList<MoteProgramException>());
		flashSem = new Semaphore(0);

		emailProperties = new Properties();

		emailProperties.setProperty("mail.smtp.host", testbedService
				.getConfig().getSmtpServer());
		emailProperties.setProperty("mail.from", testbedService.getConfig()
				.getEmailFrom());
	}

	/**
	 * Flashes a mote after setting up the ID.
	 * 
	 * @param file
	 *            The exe image to flash.
	 * @param id
	 *            The ID of a mote.
	 * @throws IOException
	 * @throws InterruptedException
	 * @throws MoteProgramException
	 */
	private void flashMote(File file, Integer id) throws IOException,
			InterruptedException, MoteProgramException {
		File out = new File(file.getAbsoluteFile() + "-" + id + ".ihex");
		log.trace("Output file " + out);
		String cmdSetSymbol[] = { "tos-set-symbols", "--objcopy",
				"msp430-objcopy", "--objdump", "msp430-objdump", "--target",
				"ihex", file.getAbsoluteFile().toString(),
				out.getAbsoluteFile().toString(), "TOS_NODE_ID=" + id,
				"ActiveMessageAddressC$addr=" + id };

		ProcessBuilder builderSymbol = new ProcessBuilder(cmdSetSymbol);
		builderSymbol.redirectErrorStream(true);
		Process processSymbol = builderSymbol.start();
		BufferedReader brSymbol = new BufferedReader(new InputStreamReader(
				processSymbol.getInputStream()));
		String line;
		while ((line = brSymbol.readLine()) != null)
			log.trace(line);

		if (processSymbol.waitFor() != 0) {
			out.delete();
			log.error("Unable to correctly execute " + cmdSetSymbol);
			throw new MoteProgramException(id,
					MoteProgramException.MoteState.SET_MOTE_ID,
					"Unable to set mote ID symbol");
		}

		log.trace(file.getAbsoluteFile() + " -> " + out.getAbsolutePath());

		FlashThread flashThread = new FlashThread(testbedService, out, id,
				receiver, this);
		flashThread.setUncaughtExceptionHandler(this);
		flashThread.start();
	}

	/**
	 * @return the flashSem
	 */
	public Semaphore getFlashSem() {
		return flashSem;
	}

	/**
	 * Fills the header for a message to be sent as report.
	 * 
	 * @return
	 * @throws AddressException
	 * @throws MessagingException
	 */
	MimeMessage getGenericMsg() throws AddressException, MessagingException {
		Session mailSession = Session.getInstance(emailProperties, null);
		MimeMessage msg = new MimeMessage(mailSession);

		msg.setFrom(new InternetAddress(testbedService.getConfig()
				.getEmailFrom()));

		msg.addRecipient(Message.RecipientType.TO, new InternetAddress(
				testbedService.getConfig().getEmailFrom()));
		for (String recipient : testbedService.getConfig().getRecipients())
			msg.addRecipient(Message.RecipientType.BCC, new InternetAddress(
					recipient));

		msg.setSentDate(new Date());
		return msg;
	}

	/**
	 * @return the receiver
	 */
	public SerialReceiver getReceiver() {
		return receiver;
	}

	/**
	 * Loads and registers serial message consumers from the configuration file.
	 * 
	 */
	private void loadConsumers() {
		for (String consumerClass : testbedService.getConfig().getConsumers()) {
			MessageConsumer consumer;
			try {
				consumer = (MessageConsumer) Class.forName(consumerClass)
						.newInstance();
			} catch (Exception e) {
				log.error(e.getMessage() + ":");
				for (StackTraceElement ste : e.getStackTrace())
					log.error(ste);
				continue;
			}
			receiver.registerMessageConsumer(consumer);
		}
	}

	/**
	 * Loads serial message classes in the JRE.
	 * 
	 * @return An array holding serial message classes.
	 */
	private ArrayList<Class<?>> loadSerialMsgClasses() {
		TestbedClassLoader cl = new TestbedClassLoader(serialMsgs);
		ArrayList<Class<?>> msgClasses = new ArrayList<Class<?>>();

		if (serialMsgs == null) {
			log.trace("No message classes to load");
			return null;
		}
		log.trace("Loading " + serialMsgs.size() + " classes");

		for (String className : serialMsgs.keySet()) {
			Class<?> c;
			try {
				c = cl.loadClass(className);
			} catch (Exception e) {
				log.error("Unable to load class " + className + ": "
						+ e.getMessage());
				for (StackTraceElement ste : e.getStackTrace())
					log.error(ste);
				continue;
			}
			serialMsgs.get(className).delete();
			log.trace("Loaded " + c.getCanonicalName());
			msgClasses.add(c);
		}
		return msgClasses;
	}

	/**
	 * Processes a task component. It basically dispatches an .exe file to
	 * multiple calls to {@link #flashMote(File, Integer, SerialReceiver)},
	 * where the .exe is converted to hex.
	 * 
	 * @param component
	 *            The stask component to process.
	 * @return A list of errors that occured during the programming.
	 * @throws MoteProgramException
	 */
	private ArrayList<MoteProgramException> processTaskComponent(
			TaskComponent component) {
		ArrayList<MoteProgramException> errors = new ArrayList<MoteProgramException>();

		File file;
		try {
			file = File.createTempFile("img", component.getFileName(),
					new File("/tmp"));
		} catch (IOException e) {
			errors.add(new MoteProgramException(-1,
					MoteProgramException.MoteState.INIT,
					"Unable to create local temporary image file.", e));
			return errors;
		}

		log.trace("Temporary image file " + file);
		FileOutputStream fw;
		try {
			fw = new FileOutputStream(file);
			fw.write(component.getFileContents());
		} catch (FileNotFoundException e) {
			file.delete();
			errors
					.add(new MoteProgramException(
							-1,
							MoteProgramException.MoteState.INIT,
							"Unable to create stream to local temporary image file.",
							e));
			return errors;
		} catch (IOException e) {
			file.delete();
			errors.add(new MoteProgramException(-1,
					MoteProgramException.MoteState.INIT,
					"Unable to save temporary image file.", e));
			return errors;
		}

		log.trace("Dumping image " + component.getFileName() + " into file "
				+ file);

		for (Integer id : component.getMotes())
			try {
				flashMote(file, id);
			} catch (IOException e) {
				errors.add(new MoteProgramException(id,
						MoteProgramException.MoteState.SET_MOTE_ID,
						"Unable to set mote id.", e));
			} catch (InterruptedException e) {
				errors.add(new MoteProgramException(id,
						MoteProgramException.MoteState.PRE_FLASH,
						"Unable to complete programming.", e));
			} catch (MoteProgramException e) {
				errors.add(e);
			}

		file.delete();
		return errors;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.util.TimerTask#run()
	 */
	@Override
	public void run() {
		log.info("Running task for " + duration + " minutes for "
				+ components.size() + " components");
		ArrayList<Class<?>> serialMsgClasses = loadSerialMsgClasses();
		if (serialMsgClasses != null) {
			receiver = new SerialReceiver(serialMsgClasses);
			loadConsumers();
		} else
			receiver = null;

		int motes = 0;

		for (TaskComponent component : components) {
			programErrors.addAll(processTaskComponent(component));
			motes += component.getMotes().size();
		}

		log.warn(programErrors);
		try {
			flashSem.acquire(motes);
		} catch (InterruptedException e) {
			log.error(e.getMessage());
		}

		log.trace("All flash tasks executed");
		sendStartReport();

		taskScheduler.scheduleEvent(new TaskTerminator(this),
				(long) duration * 60000);
	}

	/** Sends a report describing the status of the task at startup. */
	private void sendStartReport() {
		StringBuffer buf = new StringBuffer();
		buf.append(toString() + " started on " + new Date() + "\n\n");

		if (programErrors.isEmpty())
			buf.append("No programming errors encountered.\n");
		else {
			buf.append("The following errors were encountered:\n");
			for (MoteProgramException error : programErrors)
				buf.append(error + "\n");
		}

		try {
			MimeMessage msg = getGenericMsg();
			msg.setSubject(toString() + " started");
			msg.setText(buf.toString());
			Transport.send(msg);
		} catch (AddressException e) {
			log.error("Unable to recognize email address: " + e.getMessage());
		} catch (MessagingException e) {
			log.error("Unable to send email: " + e.getMessage());
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return "Task";
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * java.lang.Thread.UncaughtExceptionHandler#uncaughtException(java.lang
	 * .Thread, java.lang.Throwable)
	 */
	public void uncaughtException(Thread t, Throwable e) {
		if (!(t instanceof FlashThread)) {
			log.error("Unknown source of error: " + t);
			return;
		}
		if (!(e instanceof MoteProgramException)) {
			log.error("Unknown error: " + e.getMessage());
			for (StackTraceElement ste : e.getStackTrace())
				log.error(ste);
			return;
		}
		log.debug("Caught exception: " + e);
		programErrors.add((MoteProgramException) e);
		flashSem.release();
	}
}
